<?php
/*******************************************************************************
 * LightPress - http://lightpress.org
 *
 * Copyright 2004-2005 Ludovico Magnocavallo, Jerome Lavigne.
 * Released under the GPL http://www.gnu.org/copyleft/gpl.html
 *
 * developers:
 *              Ludovico Magnocavallo <ludo@asiatica.org>
 *              Jerome Lavigne <jerome@vapourtrails.ca>
 *
 * $Id: Frontend.php 555 2006-10-08 09:02:20Z ludo $
 ******************************************************************************/

define('LP_CONTEXT_INDEX', 1);
define('LP_CONTEXT_CATEGORY', 2);
define('LP_CONTEXT_ARCHIVES', 4);
define('LP_CONTEXT_POST', 8);
define('LP_CONTEXT_STATIC', 16);
define('LP_CONTEXT_SEARCH', 32);
define('LP_CONTEXT_AUTHOR', 64);
define('LP_CONTEXT_TAG', 128);
define('LP_CONTEXT_ALL', LP_CONTEXT_INDEX | LP_CONTEXT_CATEGORY | LP_CONTEXT_ARCHIVES |
       LP_CONTEXT_POST | LP_CONTEXT_STATIC | LP_CONTEXT_SEARCH | LP_CONTEXT_AUTHOR |
       LP_CONTEXT_TAG);
// maybe move the following to a (class) method, when (if) we add user-defined contexts
define('LP_CONTEXTS', 'index category archives post static search author tag');

class Frontend {
    
    var $_hooks = array(
        'get_posts', 'get_posts_loop', 'get_comments', 'insert_comment',
        'start_render', 'sidebar', 'parse_post', 'parse_comment',
        'render_comments', 'pre_render', 'post_render', 'shutdown');
    // set in constructor
    var $options;
    var $raw_urls = false;
    var $db;
    var $cache;
    var $tpl;
    var $messages;
    var $tables_prefix;
    var $tables;
    var $title;
    var $where_date;
    var $templates = array(
        'index'     => 'index.xml',
        'header'    => 'header.xml',
        'sidebar'   => 'sidebar.xml',
        'footer'    => 'footer.xml');
    var $_plugins = array();
    var $_plugin_dir = array();
    // getCategories
    var $_categories;
    // getCatGroups
    var $_catgroups;
    // getArchives
    var $_archives;
    // getContext
    var $context;
    // checkCategory
    var $_category_nicenames;
    // getPosts
    var $_posts;
    var $num_posts;
    var $_post_date; // cleared in getPosts, set in renderPost
    var $posts_join;
    var $posts_where;
    var $posts_in;
    // getPages
    var $_pages;
    // getPostTags
    var $_posttags;
    // getTags
    var $_tags;
    // startRender
    var $_templates_set;
    // renderPost
    var $current_post;
    // parsing helpers
    var $_block_post;
    var $_sidebar;
    // getMailHelper
    var $_mail_helper;
    // getSpamChecker
    var $_spam_checker;
    // userFromWpCookie
    var $_user_from_cookie;
    // extractKeywords;
    var $search_keywords;
    // base messages, merged with the ones from the localized file
    var $_base_messages = array(
        'no_posts'                      => 'No post found',
        'comments_open_nocomments'      => 'Be the first to add a comment to this article.',
        'comments_closed_nocomments'    => 'Comments closed.',
        'comments_closed'               => 'Comments closed.',
        'categories_header'             => 'Categories',
        'comment_accepted'              => 'Comment posted.',
        'comment_required_fields'       => 'Please fill in all required fields',
        'comment_form_message'          => "Your email address is required but won't be displayed.",
        'comment_mail_subject'          => 'New comment',
        'post_mail_subject'             => 'Reading suggestion',
        'mail_sent'                     => 'Message sent.',
        'post_continued'                => 'continued...',
        'search_title'                  => 'Search Results',
        'comment'                       => 'comment',
        'comments'                      => 'comments',
        'post'                          => 'post',
        'posts'                         => 'posts',
        'thereis'                       => 'There is',
        'thereare'                      => 'There are'
        );

    function Frontend(&$db_options, &$options, $cache=false, $view_type='html') {
        
        $this->cache =& $cache;
        $this->view_type = $view_type;
        
        /**********************************************************************
         * table names
         **********************************************************************/
        if (!isset($db_options['prefix']))
            $this->raiseError('missing tables prefix in config.php', 'Frontend', __LINE__, E_USER_ERROR);
        if (empty($db_options['prefix'])) {
            $prefix = $this->tables_prefix = '';
        } else {
            $prefix = $this->tables_prefix = $db_options['prefix'];
            $prefix .= '_';
        }
        
        $_tables = array(
            'options', 'categories', 'posts', 'post2cat', 'comments', 'users',
            'usermeta', 'postmeta', 'links', 'linkcategories', 'stats', 'tags');
        $this->tables = array();
        foreach ($_tables as $k)
            $this->tables[$k] = "${prefix}$k";
        
        /**********************************************************************
         * options
         **********************************************************************/
        
        if (!isset($options) || !is_array($options) || (count($options) == 0)) {
            
            // connection is delayed if you use fs-based options to speed up fs-based caches
            // TODO: check if this is really needed with latest DBlite versions
            $this->_connect($db_options);
            
            // retrieve from db
            $this->db->query("select option_name, option_value from " . $this->tables['options'] . " where option_name like 'lp_opt_%' or option_name in ('gmt_offset', 'wordpress_api_key')");
            
            foreach ($this->db->all() as $row)
                $options[substr($row['option_name'], 7)] = $row['option_value'];
            
            if (count($options) == 0)
                $this->raiseError('lightpress options not set in database', 'Frontend', __LINE__, E_USER_ERROR);
            
            // sanitize
            
            if (isset($options['posts_per_page']))
                $options['posts_per_page'] = (int)$options['posts_per_page'];
            if (isset($options['posts_per_rss']))
                $options['posts_per_rss'] = (int)$options['posts_per_rss'];
            if (isset($options['approx_sql_tstamp']))
                $options['approx_sql_tstamp'] = (int)$options['approx_sql_tstamp'];
            if (isset($options['plugins']) && is_string($options['plugins']) && !empty($options['plugins']))
                $options['plugins'] = unserialize($options['plugins']);
            if (isset($options['set'])) {
                // ugly but using a separate query or checking every option name would be a waste
                $options['gmt_offset'] = (int)$options['set'] * 3600;
                unset($options['set']);
            }
            if (isset($options['ss_api_key'])) {
                // same as above
                $options['wordpress_api_key'] = $options['ss_api_key'];
                unset($options['ss_api_key']);
            }
            if (!isset($options['wp_version']))
                $options['wp_version'] = '2.0.2';
            
            foreach (array('copyright_notice', 'description') as $o) {
                if (isset($options[$o]))
                    $options[$o] = stripslashes($options[$o]);
            }
            
            // add plugins if none are set
            
            if (!isset($options['plugins']) || empty($options['plugins'])) {
                $options['plugins'] = $this->getDefaultPlugins();
            }
            
        }
        
        $options['wpff_revision'] = substr(substr('$Revision: 555 $', 11), 0, -2);
        $options['wpff_version'] = $this->getVersion();
        
        $options = array_merge($this->getBaseOptions(), $options);
        $this->options =& $options;
        
        // set basedir (we cannot set it in config.php anymore since it may have been defined in the db)
        if (DIRECTORY_SEPARATOR == '/')
            ini_set('include_path', ini_get('include_path') . ':' . $options['basedir']);
        else
            ini_set('include_path', ini_get('include_path') . ';' . $options['basedir']);
        
        foreach($this->getCriticalOptions() as $option_name) {
            if (!isset($options[$option_name]))
                die("Cannot start LightPress Frontend:  missing option '$option_name' in configuration file.");
        }
        
        // set base options for those that could not be easily assigned in _base_options
        if (!isset($options['wp_url']))
            $options['wp_url'] = $options['url']; // default installation is to replace WP
        
        // guess URL for templates - templates dir should either be in site root or in the WP's lightpress plugin dir
        //  TODO: make this work for non-standard configs (or should users just ignore this var in such a case?)
        if (strstr(strtolower($options['basedir']), 'wp-content')) {
            $options['template_url'] = $options['wp_url'] . '/wp-content/plugins/lightpress/themes';
            $options['js_url'] = $options['wp_url'] . '/wp-content/plugins/lightpress/js';
        } else {
            $options['template_url'] = $options['url'] . '/themes';
            $options['js_url'] = $options['url'] . '/js';
        }
        
        /**********************************************************************
         * permalink stuff
         **********************************************************************/
        
        if (empty($options['post_prefix'])) {
            $this->raw_urls = true;
        } else {
            if (!isset($options['post_permalink_struct']))
                $options['post_permalink_struct'] = $this->prefixToPermalinkStruct($options['post_prefix']);
            if (!isset($options['archives_permalink_struct']))
                $options['archives_permalink_struct'] = $this->prefixToPermalinkStruct($options['archives_prefix']);
            $options['post_permalink_category_name'] = strpos($options['post_permalink_struct'], '%%4$s') !== false;
        }
        
        /**********************************************************************
         * db instance
         **********************************************************************/
        $this->_connect($db_options);
        
        /**********************************************************************
         * set needed attributes
         **********************************************************************/
        
        $now = time();
        if ($options['approx_sql_tstamp'] > 0)
            $now = round($now / $options['approx_sql_tstamp']) * $options['approx_sql_tstamp'];
        $this->where_date = gmdate("Y-m-d H:i:s", $now);
        $this->title = $options['blogname'];
		
        /**********************************************************************
         * localized messages
         **********************************************************************/
        $lang_tokens = explode('_', $options['lang']);
        $lang = $lang_tokens[0];
        $config_messages_file = implode(
            DIRECTORY_SEPARATOR,
            array($options['basedir'], 'lang', $lang . '_' . $options['charset'] . '.php'));
        if (is_readable($config_messages_file)) {
            require_once $config_messages_file;
            $config_messages = array_merge($this->_base_messages, $config_messages);
            $this->messages =& $config_messages;
        } else {
            // en_US UTF-8 defaults
            $this->messages =& $this->_base_messages;
        }
        
        /**********************************************************************
         * set context
         **********************************************************************/
        $this->context =& $this->getContext();
        
        /**********************************************************************
         * locale
         **********************************************************************/
        // TODO: trap wrong locale name
        setlocale(LC_TIME, $options['lang'] . '.' . $options['charset']);
        
        /**********************************************************************
         * template instance
         **********************************************************************/

        require_once 'classes/Template.php';
        $this->tpl =& new Template(sprintf(
            '%s/themes/%s/%s.%s', $options['basedir'], $options['template'], $lang, $options['charset']));

        // set options as template variables
        $tpl_options = array();
        foreach ($options as $k=>$v) {
            if (is_array($v))
                continue;
            $tpl_options["option_$k"] = $v;
        }
        if (isset($options['plugin_names']) && is_array($options['plugin_names'])) {
            foreach ($options['plugin_names'] as $p)
                $tpl_options[$p] = '';
        }
        
        $tpl_options['lp_context_type'] = $this->context['type'];
        $tpl_options['context_url'] = $this->context['url'];
        $tpl_options['page_self'] = $_SERVER['REQUEST_URI'];
        
        $this->tpl->setVar($tpl_options);
        unset($tpl_options);
        
        /**********************************************************************
         * sort out plugins
         **********************************************************************/
        $this->_plugins =& $this->getPlugins($options['plugins'], $this->context['type']);
        
    }
    
    function _connect($db_options) {
        if (!is_null($this->db))
            return;
        require_once 'classes/DBlite/MySQL.php';
        $this->db =& new DBlite_MySQL($db_options);
    }
    
    function &getPlugins($plugins_map, $current_context, $hook=null) {
        
        $plugins = array();
        
        if (is_null($hook)) {
            $hooks =& $this->_hooks;
        } else {
            $hooks = array($hook);
        }
        
        foreach ($hooks as $hook)
            $plugins[$hook] = array();
        
        $local_context = 'LP_CONTEXT_' . strtoupper($this->context['type']);
        $local_context = defined($local_context) ? constant($local_context) : LP_CONTEXT_INDEX;
        $this->local_context = $local_context;
        
        $hide_plugins = array();
        
        foreach ($plugins_map as $plugin_name=>$plugin_details) {
            list($context_type, $plugin_args) = $plugin_details;
            
            $hideplugin = true;
            if ($context_type & $local_context) {
                if (@include_once("plugins/${plugin_name}.php")) {
                    $plugin =& new $plugin_name($this, $plugin_args);
                    if ($plugin->active) {
                        foreach (array_intersect($hooks, $plugin->hooks) as $hook) {
                            $plugins[$hook][] =& $plugin;
                        }
                        $this->_plugin_dir[strtolower(get_class($plugin))] =& $plugin;
                        $hideplugin = false;
                    }
                } else {
                    // TODO: deal with missing plugins, log to a file?
                }
            }
            if ($hideplugin)
                $hide_plugins['PLUGIN_' . strtoupper($plugin_name)] = '';
        }
        
        $this->tpl->setVar($hide_plugins);
        return $plugins;
        
    }

    function &getPlugin($plugin_name) {
        $plugin_name = strtolower($plugin_name);
        if (isset($this->_plugin_dir[$plugin_name]))
            return $this->_plugin_dir[$plugin_name];
        $ret = '';
        return $ret;
    }
    
    function _processPlugins($hook, &$hook_data, $hide=false) {
        
        // render plugins
        $plugins =& $this->_plugins;
        
        if (isset($plugins[$hook])) {
            foreach ($plugins[$hook] as $k => $v) {
                if ($hide)
                    $plugins[$hook][$k]->hide();
                else
                    $plugins[$hook][$k]->run($hook, $hook_data);
            }
        }
        
    }
    
    /***************************************************************************
     * get data methods
     **************************************************************************/
    
    function &getCategories($force=false) {
        /*
         * gets blog categories and builds the structures needed to parse
         * the menu, and posts
         *
         * if you call this method manually before the parse methods, the
         * structures are cached and reused later
         */
        if (is_null($this->_categories) || $force) {
            
            $tables =& $this->tables;
            $q = "
                select
                    cat_id, category_parent, cat_name, category_nicename,
                    category_description, category_count as numposts
                from
                    {$tables['categories']}
                order
                    by cat_name
                ";
            
                $this->db->query($q);
            
            $categories = array();
            $catgroups = array();
            
            foreach ($this->db->all() as $category) {
                if ($this->raw_urls)
                    $category['category_permalink'] = 'index.php?c=' . $category['cat_id'];
                else
                    $category['category_permalink'] = $this->options['category_prefix'] . '/' . $category['category_nicename'];
                $categories[$category['cat_id']] = $category;
            }
            
            $this->_categories =& $categories;
            
        }

        return $this->_categories;
    }
    
    function &getArchives($force=false) {
        /*
         * gets blog archives and builds the structure used later to parse
         * the sidebar
         *
         * if you call this method manually before the parse methods, the
         * structures are cached and reused later
         */
        if (is_null($this->_archives) || $force) {
            
            if (!$force && !is_null($this->cache) && $this->cache->active) {
                // try to fetch from the cache
                $archives = $this->cache->getCache('_frontend_get_archives', '', false);
                if (!empty($archives)) {
                    $archives = @unserialize($archives);
                    if (is_array($archives)) {
                        $this->_archives =& $archives;
                        return $archives;
                    }
                }
            }
            
            $tables =& $this->tables;
            $q = "select year(post_date) as y, lpad(month(post_date), 2, '0') as m, count(*) as numposts
            from " . $tables['posts'] . " where post_status = 'publish' and post_date_gmt <= '" . $this->where_date . "'
            group by y, m
            order by y desc, m desc";

            $result = $this->db->query($q);
            
            $archives = array();
            $archive_label =& $this->options['archive_label'];
            $archive_prefix =& $this->options['archives_prefix'];
            // PHP tends to forget the locale
            setlocale(LC_TIME, $this->options['lang'] . '.' . $this->options['charset']);
            foreach ($this->db->all() as $m) {
                $month = (int)$m['m'];
                $m['month'] = $month;
                $archive_date = mktime(0, 0, 0, $month, 1, (int)$m['y']);
                $m['date'] = $archive_date;
                $m['label'] = strftime($archive_label, $archive_date);
                if ($this->raw_urls)
                    $m['permalink'] = 'index.php?m=' . $m['y'] . $m['m'];
                else
                    $m['permalink'] = strftime($this->options['archives_permalink_struct'], $archive_date);
                $date = $m['y'] . sprintf("%02d", $month);
                $archives[$date] = $m;
            }
            
            $this->_archives =& $archives;
            
            if (!is_null($this->cache) && $this->cache->active) {
                $_archives = serialize($archives);
                $this->cache->setCache($_archives, '_frontend_get_archives', '', false);
            }

        }

        return $this->_archives;
    }
    
    function &getContext($context_type=null, $context_filter=null, $context_index=0, $context_reqtype='') {
        /*
         * context type is post/category/archives/static/search/author, etc.
         * context filter is the key used to retrieve the main object (eg post name)
         * context index is the starting index when displaying lists of objects
         * context reqtype is used to distinguish all-purpose plugins from specialized (eg html/feed plugins)
         *
         */
        $tables =& $this->tables;
        $options =& $this->options;
        
        $context = array(
            'type'      => $context_type,
            'filter'    => $context_filter,
            'hierarchy' => array(),
            'index'     => $context_index,
            'url'       => '',
            'label'     => '',
            'reqtype'   => $context_reqtype,
            'queryvars' => array('f'=>'', 'j'=>" RIGHT JOIN {$tables['users']} u on p.post_author=u.id ", 'w'=>'', 'g'=>''));

        /***************************************************************************
         * context type and filter
         **************************************************************************/
        // shortcut context for the WP 2.0 'preview' feature
        if (isset($_GET['preview']) && $_GET['preview'] == 'true' && isset($_GET['p'])) {
            $user = $this->userFromWpCookie();
            if (!is_null($user)) {
                $preview_id = strip_tags($_GET['p']);
                if ($user['user_level'] > 4 || isset($user['capabilities']['administrator']) || isset($user['capabilities']['editor'])) {
                    $context['type'] = 'preview';
                    $context['filter'] = $preview_id;
                } else if ($this->db->query("select 1 from {$this->tables_prefix}_posts where ID = $preview_id and post_author = {$user['id']}")) {
                    $context['type'] = 'preview';
                    $context['filter'] = $preview_id;
                }
                if ($context['type'] == 'preview') {
                    // fix the date used in where clauses to display future posts, thanks Klaas
                    $this->where_date = gmdate("Y-m-d H:i:s", time() + (365 * 24 * 60 * 60));
                }
            }
        }
        
        if (is_null($context['type']) || is_null($context['filter'])) {

            $requestvars = array(
                'p'=>'post', 'pi'=>'post_id', 'c'=>'category', 'a'=>'archives',             // LP vars
                's'=>'static', 'q'=>'search', 'u' =>'author', 'y'=>'archives', 'tag'=>'tag',// LP vars
                'm'=>'archives', 'postname'=>'post', 'category_name'=>'category',           // old WP vars
                'pagename'=>'static', 'year'=>'archives', 'page_id'=>'static');             // old WP vars
            
            $req = array_merge($_POST, $_GET);
            
            foreach ($requestvars as $k=>$v) {
                if (isset($req[$k]) && !empty($req[$k])) {
                    $context['type'] = $v;
                    $context['filter'] = strip_tags($req[$k]);
                    break;
                }
            }
            
            // fix static context so that we use the id
            if ($context['type'] == 'static') {
                $pages =& $this->getPages();
                foreach ($pages as $k=>$v) {
                    // no hierarchy, so we cannot check for duplicate post names
                    if ($v['post_name'] == $context['filter']) {
                        // make the tagcloud plugin work by simulating a hierarchical request
                        $context['hierarchy'] = array($context['filter'], null, null);
                        $context['filter'] = $v['id'];
                        break;
                    }
                }
            }
            
            /***************************************************************************
             * nested contexts
             **************************************************************************/
            if (empty($context['type']) && isset($_GET['y1'])) {
                $y1 = strip_tags($_GET['y1']);
                $y2 = (isset($_GET['y2']) ? strip_tags($_GET['y2']) : null);
                $y3 = (isset($_GET['y3']) ? strip_tags($_GET['y3']) : null);
                
                $parent = $subparent = $child = null;
                
                // try pages first
                $pages =& $this->getPages();
                
                foreach ($pages as $k=>$v) {
                    switch ($v['post_name']) {
                        case $y1:
                            $parent =& $pages[$k];
                            break;
                        case $y2:
                            if (isset($pages[$v['post_parent']]) && $pages[$v['post_parent']]['post_name'] == $y1)
                                $subparent =& $pages[$k];
                            break;
                        case $y3:
                            if (!isset($pages[$v['post_parent']]) || $pages[$v['post_parent']]['post_name'] != $y2)
                                break;
                            $sp =& $pages[$v['post_parent']];
                            if (!isset($pages[$sp['post_parent']]) || $pages[$sp['post_parent']]['post_name'] != $y1)
                                break;
                            $child =& $pages[$k];
                            break;
                    }
                }
                
                if ($child) {
                    $context['type'] = 'static';
                    $context['filter'] = $child['id'];
                    $context['url'] = $this->getPagePermalink(
                        $child['id'], $child['post_name'],
                        is_null($subparent) ? '' : $subparent['post_name'],
                        is_null($parent) ? '' : $parent['post_name']);
                } else if ($subparent) {
                    $context['type'] = 'static';
                    $context['filter'] = $subparent['id'];
                    $context['url'] = $this->getPagePermalink(
                        $subparent['id'], $subparent['post_name'],
                        is_null($parent) ? '' : $parent['post_name'],
                        '');
                } else if ($parent) {
                    $context['type'] = 'static';
                    $context['filter'] = $parent['id'];
                    $context['url'] = $this->getPagePermalink(
                        $parent['id'], $parent['post_name'],
                        '',
                        '');
                } else {
                    // try categories
                    
                    $categories =& $this->getCategories();
                    
                    foreach ($categories as $k=>$v) {
                        switch ($v['category_nicename']) {
                            case rawurlencode($y1):
                                $parent =& $categories[$k];
                                break;
                            case rawurlencode($y2):
                                $subparent =& $categories[$k];
                                break;
                            case rawurlencode($y3):
                                $child =& $categories[$k];
                                break;
                        }
                    }
                    
                    if ($child) {
                        $context['type'] = 'category';
                        $context['filter'] = $child['cat_id'];
                    } else if ($subparent) {
                        $context['type'] = 'category';
                        $context['filter'] = $subparent['cat_id'];
                    } else if ($parent) {
                        $context['type'] = 'category';
                        $context['filter'] = $parent['cat_id'];
                    } else {
                        // TODO: output a customized error page
                        if (isset($_SERVER['SERVER_PROTOCOL']))
                            header ($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
                        else
                            header ('HTTP/1.0 404 Not Found');
                    }
                }
                // we might as well store the hierarchy, might be useful to plugins
                $context['hierarchy'] = array(&$parent, &$subparent, &$child);
            }
            
            if (ini_get('magic_quotes_gpc') == 1)
                $context['filter'] = stripslashes($context['filter']);
        }
        
        /***************************************************************************
         * context index
         **************************************************************************/
        
        if ($context['index'] == 0) {
            if (isset($_REQUEST['i']) && !empty($_REQUEST['i'])) {
                $context['index'] = (int)$_REQUEST['i'];
                if ($_REQUEST['i'] != '0' && $context['index'] == 0)
                    $context['index'] = $_REQUEST['i'];
            } else if (isset($_REQUEST['paged']) && !empty($_REQUEST['paged'])) {
                $context['index'] = (int)$_REQUEST['paged'] * (int)$this->options['posts_per_page'];
            }
        }
        
        /***************************************************************************
         * reqtype
         **************************************************************************/
        
        if (empty($this->_reqtype) && isset($_GET['reqtype']))
            $context['reqtype'] = $_GET['reqtype'];
        
        /***************************************************************************
         * fix/check filter/url and set queryvars
         **************************************************************************/
        
        switch ($context['type']) {
            case 'post':
                if ($this->raw_urls) {
                    // use WP-style post permalink, e.g. index.php?p=id
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.id='{$context['filter']}'";
                } else {
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.post_name='" . urlencode($context['filter']) . "'";
                }
                // delay setting up context_url
                $context['url'] = null;
                break;
            case 'preview':
                $context['type'] = 'post';
                $context['queryvars']['w'] = "AND (p.id='{$context['filter']}' OR p.post_name='{$context['filter']}')";
                // delay setting up context_url
                $context['url'] = null;
                break;
            case 'post_id':
                $context['type'] = 'post';
                $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.id='{$context['filter']}'";
                // delay setting up context_url
                $context['url'] = null;
                break;
            case 'category':
                // we avoid a join in the posts query, and swap the shortname
                // so as to be able to access categories by index
                // a second loop since the index page is accessed more
                $categories =& $this->getCategories();
                $found = false;
                if (!isset($categories[$context['filter']])) {
                    // WP encodes the shortname in the DB, ugh!
                    $nicename = rawurlencode($context['filter']);
                    foreach ($categories as $cat_id => $category) {
                        if ($category['category_nicename'] == $nicename) {
                            $context['filter'] = $cat_id;
                            $context['label'] = $category['cat_name'];
                            $this->title .= " &raquo; " . $category['cat_name'];
                            $found = true;
                            break;
                        }
                    }
                } else {
                    $found = true;
                    $context['label'] = $categories[$context['filter']]['cat_name'];
                    $this->title .= " &raquo; " . $categories[$context['filter']]['cat_name'];
                }
                if (!$found) {
                    $context['type'] = 'post';
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.id=''";
                } else {
                    $context['url'] = $categories[$context['filter']]['category_permalink'];
                    $context['queryvars']['j'] .= " INNER JOIN " . $tables['post2cat'] . " p2c on p.id=p2c.post_id ";
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p2c.category_id='{$context['filter']}'";
                }
                break;
            case 'archives':
                if (!$this->raw_urls) {
                    // fix context_filter if we have a 'y' or 'year' var
                    if (isset($_REQUEST['y']) && isset($_REQUEST['m'])) {
                        $context['filter'] .= strip_tags($_REQUEST['m']);
                    } elseif (isset($_REQUEST['year']) && isset($_REQUEST['monthnum'])) {
                        $context['filter'] .= strip_tags($_REQUEST['monthnum']);
                    }
                }
                $archives =& $this->getArchives();
                if (!in_array($context['filter'], array_keys($archives))) {
                    $context['type'] = 'post';
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.id=''";
                } else {
                    $archive =& $archives[$context['filter']];
                    $this->title .= " &raquo; " . $archive['label'];
                    $context['label'] = $archive['label'];
                    $context['url'] = $archive['permalink'];
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND year(p.post_date)=" . $archive['y'] .
                        " AND month(p.post_date)=" . $archive['m'] . " ";
                }
                break;
            case 'static':
                $context['queryvars']['w'] = "AND (p.post_status='static') AND p.id='{$context['filter']}'";
                $context['queryvars']['j'] .= ' LEFT JOIN ' . $tables['posts'] . ' AS parent ON p.post_parent = parent.ID ';
                $context['queryvars']['f'] = ', parent.post_name AS parent_name ';
                // delay setting up context_url as we do not have the id yet
                $context['url'] = null;
                break;
            case 'search':
                $context['label'] = "{$this->messages['search_title']}: {$context['filter']}";
                $this->title .= " &raquo; " . $this->messages['search_title'];
                $kw =& $this->_extractKeywords($context['filter']);
                if (count($kw) > 0) {
                    $context['queryvars']['j'] .= " LEFT JOIN {$tables['tags']} AS tags ON (p.ID = tags.post_id) ";
                    $context['queryvars']['w'] .= "
                        AND (p.post_status='publish' OR p.post_status='static') AND (
                            (p.post_title LIKE '%" . implode("%' AND p.post_title LIKE '%", $kw) . "%')
                            OR 
                            (p.post_content LIKE '%" . implode("%' AND p.post_content LIKE '%", $kw) . "%')
                            OR 
                            (tags.tag_name='" . implode("' OR tags.tag_name='", $kw) . "')
                        ) ";
                }
                if ($this->raw_urls)
                    $context['url'] = 'index.php?q=' . urlencode($context['filter']);
                else
                    $context['url'] = $options['search_prefix'] . '/' . urlencode($context['filter']);
                break;
            case 'author':
                $found = false;
                $this->db->query("select id, display_name from {$this->tables['users']}");
                foreach ($this->db->all() as $row) {
                    if ($this->sanitize_user($row['display_name']) == $context['filter']) {
                        $context['filter'] = $row['id'];
                        $context['label'] = $row['display_name'];
                        $this->title .= ' &raquo; ' . $context['filter'];
                        $context['queryvars']['w'] = "AND (p.post_status='publish') AND u.id={$context['filter']}";
                        if ($this->raw_urls)
                            $context['url'] = 'index.php?u=' . urlencode($context['filter']);
                        else
                            $context['url'] = $options['author_prefix'] . '/' . $this->sanitize_user($context['filter']);
                        $found = true;
                        break;
                    }
                }
                if (!$found) {
                    $context['type'] = 'post';
                    $context['queryvars']['w'] = "AND (p.post_status='publish') AND p.id=''";
                }
                break;
            case 'tag':
                $context['label'] = $context['filter'];
                $this->title .= ' &raquo; ' . $context['filter'];
                $context['url']  = $this->getTagPermalink($context['filter']);
                $context['queryvars']['j'] .= " LEFT JOIN {$tables['tags']} AS tags ON (p.ID = tags.post_id) ";
                $context['queryvars']['w'] = " AND (p.post_status='publish' OR p.post_status='static') AND (tags.tag_name='" . addslashes($context['filter']) . "') ";
                break;
            default:
                $context['queryvars']['w'] = "AND (p.post_status='publish')";
                break;
        }
        
        if (!is_null($context['url']))
            $context['url'] = $options['url'] . (($context['url'] != '') ? '/' : '') . $context['url'];
        
        return $context;
    }
    
    function getLastModified($check_comments=false) {
        
        $db =& $this->db;
        $tables =& $this->tables;
        
        list($qv_f, $qv_j, $qv_w, $qv_g) = array_values($this->context['queryvars']);
        
        $field = "p.post_modified_gmt";

        if ($check_comments) {
            $field = "c.comment_date_gmt";
            $qv_j .= " RIGHT JOIN " . $tables['comments'] . " c ON c.comment_post_id=p.id ";
        }
        
        $q = "SELECT UNIX_TIMESTAMP(MAX($field)) as tstamp, MAX($field) as lastmoddate
            FROM {$tables['posts']} p
            $qv_j
            WHERE p.post_date_gmt <= '{$this->where_date}' $qv_w";
        
        $db->query($q);
        $row = $db->next();
        
        if ($row == null || !isset($row['tstamp']))             // no timestamp, generate one and subtract gmt_offset
            return time() - $this->options['gmt_offset'];
        else
            return (int)$row['tstamp'];                      // GMT timestamp
    }
    
    function &getPosts($context_type=null, $context_filter=null, $start_record=null, $num_records=null, $force=false, $simple=false) {

        $context = null;

        // TODO: fix the following
        if ((!is_null($context_type) && $context_type != $this->context['type']) || (!is_null($context_filter) && $context_filter != $this->context['filter']))
            $context =& $this->getContext($context_type, $context_filter);
        
        if (is_null($context)) {
            // check if we can return the cached result
            if (!is_null($this->_posts) && !$force)
                return $this->_posts;
            else
                $context =& $this->context;
        }
            
        $context_type =& $context['type'];
        $context_filter =& $context['filter'];
            
        $options =& $this->options;
        $tables =& $this->tables;

        // render plugins
        $this->_processPlugins('get_posts', $context['queryvars']);
        list($f, $j, $w, $g) = array_values($context['queryvars']);

        if (is_null($num_records))
            $num_records = $options['posts_per_page'];
        if (is_null($start_record))
            $start_record = $context['index'];
                
        $set_title = (($context_type == 'post') || ($context_type == 'static'));
        
        /*******************************************************************
         * create the context-dependent query
         ******************************************************************/

        if ($context_type == 'post' || $context_type == 'static')   // save the count(*) query
            $num_records = 1;
        
        /*******************************************************************
         * retrieve posts
         ******************************************************************/
        
        $q = "SELECT
        p.id as post_id, p.post_author as post_author_id,
        UNIX_TIMESTAMP(p.post_date_gmt) + '" . $this->options['gmt_offset'] . "' as post_tstamp,
        UNIX_TIMESTAMP(p.post_date_gmt) as post_tstamp_gmt, p.post_excerpt, 
        p.post_title, p.post_content, p.post_content_filtered, p.post_status, p.comment_status, p.ping_status,
        p.post_name, p.guid as post_guid, p.post_category, p.comment_count as post_comments, u.display_name as user_display_name, u.user_nicename,
        u.user_login as post_author_login
        $f
        FROM " . $tables['posts'] . " p
        $j
        WHERE
        p.post_date_gmt <= '" . $this->where_date . "'
        $w
        GROUP BY
        p.id, p.post_author, post_tstamp, post_tstamp_gmt, p.post_title, p.post_content,
        p.post_status, p.comment_status, p.ping_status, p.post_name, p.guid, p.post_category,
        u.display_name, u.user_nicename, u.user_login
        $g
        ORDER BY
        p.post_date DESC " .
        (($context_type != 'post' && $context_type != 'static' && $num_records > 0) ? "LIMIT $start_record, $num_records" : "");
        
        $this->db->query($q);
        $posts = $this->db->all();
            
        /*******************************************************************
         * store posts count
         ******************************************************************/
        $num_posts = $this->db->count; // used also in the posts loop below
        
        switch ($num_records) {
            case 0:
            case 1:
                $this->num_posts = $num_posts;
                if (is_null($this->context['url'])) {           // might have been set when checking the hierarchy context
                    $this->context['url'] = $options['url'];    // no posts, set it to the index URL
                    if (($num_records == 1) && isset($posts[0])) {
                        if ($posts[0]['post_status'] == 'static')
                            $this->context['url'] .= '/' . $this->getPagePermalink($posts[0]['post_id'], $posts[0]['post_name']);
                        else
                            $this->context['url'] .= '/' . $this->getPermalink($posts[0]['post_id'], $posts[0]['post_name'], $posts[0]['post_tstamp'], $posts[0]['post_category']);
                    }
                }
                
                if (empty($context['label']) && count($posts))
                    $context['label'] = $posts[0]['post_title'];
                
                break;
            default:
                $q = "SELECT count(*) as num from " . $tables['posts'] . " p $j WHERE p.post_date_gmt <= '" . $this->where_date . "' $w";
                $this->db->query($q);
                $this->num_posts = (int)$this->db->get('num');
        }
            
        if ($num_posts == 0)
            return $posts;
            
        /*******************************************************************
         * create the where filter for later queries (e.g. categories and comments)
         ******************************************************************/
                
        $posts_in = array();
        foreach ($posts as $post)
            $posts_in[] = $post['post_id'];
        
        // store in context as well
        
        $this->posts_in = $posts_in = implode(',', $posts_in);
        
        if (!$simple) {    

            /*******************************************************************
             * retrieve posts categories
             ******************************************************************/
                
            $q = "select distinct post_id, category_id from " . $tables['post2cat'] . " " .
                "where post_id in ($posts_in) order by post_id, category_id";
                
            $this->db->query($q);
                
            $posts_categories = array();
            foreach ($this->db->all() as $p2c) {
                $post_id = $p2c['post_id'];
                if (!isset($posts_categories[$post_id]))
                    $posts_categories[$post_id] = array();
                $posts_categories[$post_id][] = $p2c['category_id'];
            }
            
            /*******************************************************************
             * retrieve posts' tags
             ******************************************************************/
            
/*            $q = "SELECT DISTINCT post_id, tag_name AS name
                    FROM {$tables['tags']} tag
                    WHERE post_id IN ({$this->posts_in})
                    ORDER BY post_id, name";
            $this->db->query($q);
            
            $posts_tags = array();
            foreach ($this->db->all() as $tag) {
                $post_id = $tag['post_id'];
                if (!isset($posts_tags[$post_id]))
                    $posts_tags[$post_id] = array();
                $posts_tags[$post_id][] = $tag['name'];
            }
*/        
        }
            
        /*******************************************************************
         * loop through posts and fill in missing attributes
         ******************************************************************/
            
        $messages =& $this->messages;
            
        for ($i=0; $i<$num_posts;$i++) {
            $post =& $posts[$i];
            $tstamp = (int)$post['post_tstamp'];
            $post['post_excerpt'] = stripslashes($post['post_excerpt']);
            if (
                $this->options['use_preformatter'] &&
                !empty($post['post_content_filtered']) &&
                substr($post['post_content_filtered'], -1) == chr(0)
                ) {
                $content = explode('<!--more-->', stripslashes(substr($post['post_content_filtered'], 0, -1)), 2);
                $post['post_content'] = $content[0];
                $post['post_more'] = (count($content) > 1) ? '<a name="more"></a>' . $content[1] : '';
            } else {
                $content = explode('<!--more-->', stripslashes($post['post_content']), 2);
                $post['post_content'] = $this->paragrapher($content[0]);
                $post['post_more'] = (count($content) > 1) ? '<a name="more"></a>' . $this->paragrapher($content[1]) : '';
            }
            $post['post_title'] = stripslashes($post['post_title']);
            if ($set_title)
                $this->title .= ' &raquo; ' . $post['post_title'];
            $post['post_title_xmlsafe'] = htmlspecialchars($post['post_title'], ENT_QUOTES);
            $post['post_date'] = strftime($options['date_format'], $tstamp);
            $post['post_short_date'] = strftime($options['short_date_format'], $tstamp);
            $post['post_time'] = strftime($options['time_format'], $tstamp);
            $post['post_short_time'] = strftime($options['short_time_format'], $tstamp);
            $post['post_tstamp'] = $tstamp;
            if ($post['post_status'] == 'static')
                $post['post_permalink'] = $this->getPagePermalink($post['post_id'], $post['post_name']);
            else
                $post['post_permalink'] = $this->getPermalink($post['post_id'], $post['post_name'], $tstamp, $post['post_category']);
            $post['post_year'] = strftime("%Y", $tstamp);
            $post['post_month'] = strftime("%m", $tstamp);
            $post['post_day'] = strftime("%d", $tstamp);
            $post['post_author'] = empty($post['user_display_name']) ? $post['user_nicename'] : $post['user_display_name'];
            $post['post_author_ascii'] = $this->sanitize_user($post['user_display_name']);
            if (!$simple) {
                $post['post_comments_label'] = (int)$post['post_comments'] == 1 ? $messages['comment'] : $messages['comments'];
                if ($context_type != 'static' && isset($posts_categories[$post['post_id']]))
                    $post['categories'] =& $posts_categories[$post['post_id']];
                else
                    $post['categories'] = array();
/*                if (isset($posts_tags[ $post['post_id'] ]))
                    $post['tags'] = $posts_tags[ $post['post_id'] ];
                else
                    $post['tags'] = array();
*/            }
            $post['post_class_suffix'] = ($i == ($num_posts - 1)) ? '_last' : '';
            // render plugins
            $this->_processPlugins('get_posts_loop', $post);
        }
        if (is_null($context)) {
            $this->_posts =& $posts;
            $this->_post_date = null;
            return $this->_posts;
        } else {
            return $posts;
        }
    }
    
    function &getPages() {
        
        if (is_null($this->_pages)) {
            $q = "select p.id, p.post_name, p.post_title, p.post_parent, " .
                "UNIX_TIMESTAMP(p.post_date_gmt) + '" . $this->options['gmt_offset'] . "' as post_tstamp " .
                "from " . $this->tables['posts'] . " p " .
                "where p.post_status='static' and p.post_date_gmt <= '" . $this->where_date . "' " .
                "order by p.post_parent, p.post_title";
            $this->db->query($q);

            $_pages =& $this->db->all();
            $pages = array();
            $deferred = array();

            foreach ($_pages as $k=>$page) {
                $parent = $page['post_parent'];
                $_pages[$k]['root_page'] = empty($parent);
                $_pages[$k]['children'] = array();
                if (!empty($parent)) {
                    if (isset($pages[$parent]))
                        $pages[$parent]['children'][$page['post_title']] =& $_pages[$k];
                    else
                        $deferred[$page['id']] = array($page['post_title'], $parent);
                }
                $pages[$page['id']] =& $_pages[$k];
            }

            foreach ($deferred as $id=>$details) {
                list($title, $parent) = $details;
                if (isset($pages[$parent])) {
                    $pages[$parent]['children'][$title] =& $pages[$id];
                } else
                    $pages[$id]['parent'] = '';
            }
            
            // we use the title as a key for children to ease sorting,
            // but we don't sort here to skip a second recursion that
            // as it may not be used in the current context
            
            $this->_pages =& $pages;
        }
        return $this->_pages;
    }
    
    function getPagePermalink($post_id, $post_name, $parent_name=null, $ancestor_name=null) {
        
        if ($this->raw_urls)
            return 'index.php?page_id=' . $post_id;
        
        $options =& $this->options;
        
        if (!empty($options['pages_prefix']))
            return $options['pages_prefix'] . '/' . $post_name . $options['pages_suffix'];

        // build hierarchy
        if (is_null($parent_name) || is_null($ancestor_name)) {
            $pages =& $this->getPages();
            if (!isset($pages[$post_id]))
                return;
            $tokens = array();
            $id = $post_id;
            while (isset($pages[$id])) {
                $tokens[] = $pages[$id]['post_name'];
                $id = $pages[$id]['post_parent'];
            }
            $tokens = array_reverse($tokens);
        } else {
            $tokens = array();
            if (!empty($ancestor_name))
                $tokens[] = $ancestor_name;
            if (!empty($parent_name))
                $tokens[] = $parent_name;
            $tokens[] = $post_name;
        }
        
        return implode('/', $tokens) . $options['pages_suffix'];
    }
    
    function getPermalink($post_id, $post_name, $post_tstamp, $category_id) {
        $options =& $this->options;
        
        if ($this->raw_urls)
            return 'index.php?p=' . $post_id;
        
        $category_name = 'uncategorized';
        if ($this->options['post_permalink_category_name']) {
            $categories =& $this->getCategories();
            if (isset($categories[$category_id]))
                $category_name = $categories[$category_id];
        }
        return sprintf(
            strftime($this->options['post_permalink_struct'], $post_tstamp),
            $post_name, $post_id, $category_id, $category_name);
    }
    
    function &getExcerpt(&$text, $maxlength=0, $continue='…') {
        // treat text as if it were post_content, assume slashes have already been stripped
        foreach (explode('<!--more-->', $text) as $t) {
            $buffer =& $t;
            break;
        }
        $buffer = strip_tags($buffer);
        if ($maxlength) {
            $tokens = preg_split('/\s+/u', $buffer);
            if (count($tokens) > $maxlength)
                $buffer = implode(' ', array_slice($tokens, 0, $maxlength)) . $continue;
        }
        return $buffer;
    }
    
    function &getComments($post_id=null, $start_record=null, $num_records=null) {
        
        $payload = array('start'=>$start_record, 'num'=>$num_records, 'post_id'=>$post_id);

        // render plugins
        $this->_processPlugins('get_comments', $payload);

        $start_record = $payload['start'];
        $num_records = $payload['num'];
        
        $q = "select *,
            UNIX_TIMESTAMP(comment_date_gmt) + '" . $this->options['gmt_offset'] . "' as comment_timestamp,
            UNIX_TIMESTAMP(comment_date_gmt) as comment_tstamp_gmt
            from " .
            $this->tables['comments'] .
            " where " .
            (!is_null($post_id) ? "comment_post_id=" . $post_id : '') .
            " and comment_approved='1' order by comment_date";

        if (!is_null($num_records)) {
            $start_record = is_null($start_record) ? 0 : $start_record;
            $q .= " limit $start_record, $num_records";
        }

        $db =& $this->db;
        $db->query($q);
        $comments =& $db->all();
        return $comments; // avoid the stupid PHP warning
    }

    function &getPostTags($postid=null, $force=false) {
        /*
         * caches all tags matching current post selection
         *
         * if you call this method manually before the parse methods, the
         * structures are cached and reused later
         */
        if (is_null($this->_posttags) || $force) {
            if (!isset($this->posts_in))
                $this->getPosts();    // force posts cache load
            
            if (empty($this->posts_in)) {
                $this->_posttags = array();
                return $this->_posttags;
            }
                
            $tables =& $this->tables;
            $q = "SELECT DISTINCT post_id, tag_name AS name
                    FROM {$tables['tags']} tag
                    WHERE post_id IN ({$this->posts_in})
                    ORDER BY post_id, name";
            $this->db->query($q);
            
            $posts_tags = array();
            foreach ($this->db->all() as $tag) {
                $post_id = $tag['post_id'];
                if (!isset($posts_tags[$post_id]))
                    $posts_tags[$post_id] = array();
                $posts_tags[$post_id][] = $tag['name'];
            }
            $this->_posttags =& $posts_tags;
        }
        if (is_null($postid))
            return $this->_posttags;
        elseif (isset($this->_posttags[$postid]))
            return $this->_posttags[$postid];
        else {
            $ret = array();
            return $ret;
        }
    }

    function getTagEncoded($tagname) {
        return(str_replace('%2F', '/', urlencode($tagname)));
    }
    
    function getTagPermalink($tagname) {
        return ( ($this->raw_urls) ? 'index.php?tag=' : $this->options['tag_prefix'] . '/')
               . $this->getTagEncoded($tagname);
    }
    
    function &getTags($force=false) {
        /*
         * gets all site tags
         *
         * if you call this method manually before the parse methods, the
         * structures are cached and reused later
         */
        if (is_null($this->_tags) || $force) {
            $tables =& $this->tables;
            $q = "SELECT tag.tag_name AS name, COUNT(tag.post_id) as numposts,
                    UNIX_TIMESTAMP(max(p.post_date_gmt)) + '" . $this->options['gmt_offset'] . "' as last_post_date,
                    UNIX_TIMESTAMP(max(p.post_date_gmt)) as last_post_date_gmt
                FROM {$tables['tags']} tag
                INNER JOIN {$tables['posts']} p on tag.post_id=p.id
                WHERE (p.post_status='publish' OR p.post_status='static') AND p.post_date_gmt<='{$this->where_date}'
                GROUP BY tag.tag_name
                ORDER BY numposts DESC ";
            $this->db->query($q);
            
            $tags = array();
            
            foreach ($this->db->all() as $tag) {
                $tag['encoded'] = $this->getTagEncoded($tag['name']);
                $tag['permalink'] = $this->getTagPermalink($tag['name']);
                $tags[$tag['name']] = $tag;
            }
            $this->_tags =& $tags;
        }
        return $this->_tags;
    }
    
    /***************************************************************************
     * render methods
     **************************************************************************/
    
    function startRender($content=null, $header=null, $sidebar=null, $footer=null) {
        if (!$this->_templates_set) {

            $templates =& $this->templates;
        
            if (is_null($content)) {
                switch ($this->context['type']) {
                    case 'search':
                    case 'author':
                    case 'tag':
                    case 'static':
                    case 'post':
                        $templates['index'] = $this->context['type'] . '.xml';
                        break;
                    default:
                        // already set in $this->templates;
                        //$templates['index'] = 'index.xml';
                }
            } else {
                $templates['index'] = $content;
            }
            
            if (!is_null($header))
                $templates['header'] = $header;
            if (!is_null($sidebar))
                $templates['sidebar'] = $sidebar;
            if (!is_null($footer))
                $templates['footer'] = $footer;
            
            $this->_processPlugins('start_render', $templates);
            
            $this->tpl->setFile($templates);
			$this->_templates_set = true;
        }
    }
    
    function renderPost(&$post, $parse_block=true) {
        $this->current_post =& $post;
        $tpl =& $this->tpl;
        $context =& $this->context;
        $this->startRender();
        if ($parse_block && !$this->_block_post) {
            // set blocks
            $tpl->setBlock('index', 'post', 'BLOCK_POST');
            // TODO: make the following optional?
            $tpl->setBlock('post', 'day', 'BLOCK_DAY');
            $tpl->setBlock('post', 'post_category', 'BLOCK_POST_CATEGORY');
            $tpl->setBlock('post', 'post_more', 'BLOCK_POST_MORE');
            $this->_block_post = true;
        }
        $post['skip_form'] = $post['skip_comments'] = false;
        /*
         * display the date label if necessary, or hide its template slot
         */
        $static = false;
        switch ($context['type']) {
            case 'static':
                $static = true;
            case 'post':
                // hide the day block
                $tpl->setVar('BLOCK_DAY', '');
                // hide the 'read more' link
                $tpl->setVar('BLOCK_POST_MORE', '');
                // display comments
                $this->renderComments($post);
                break;
            default:
                // index pages, check date change
                if ($post['post_date'] != $this->_post_date) {
                    // display the date
                    $tpl->setVar('day_date', $post['post_date']);
                    $tpl->parse('BLOCK_DAY', 'day');
                    $this->_post_date = $post['post_date'];
                    $post['post_class_first'] = '_first';
                } else {
                    // hide the date
                    $tpl->setVar('BLOCK_DAY', '');
                    $post['post_class_first'] = '';
                }
        }
        /*
         * display post categories
         */
        if (!$static) {
            // categories
            $multicategory = false;
            $categories =& $this->getCategories();
            $i = count($post['categories']);
            $sep = $this->options['category_separator'];
            foreach ($post['categories'] as $post_category) {
                $tpl->setVar(array(
                    'post_cat_id'           => $post_category,
                    'post_cat_name'         => $categories[$post_category]['cat_name'],
                    'post_cat_nicename'     => $categories[$post_category]['category_nicename'],
                    'post_cat_permalink'    => $categories[$post_category]['category_permalink'],
                    'post_cat_separator'    => ($i != 1 ? $sep : '')));
                $tpl->parse('BLOCK_POST_CATEGORY', 'post_category', $multicategory);
                $multicategory = true;
                $i--;
            }
        } else {
            $tpl->setVar('BLOCK_POST_CATEGORY', '');
        }
        $this->_processPlugins('parse_post', $post);
        $tpl->setVar($post);
        if ($context['type'] != 'post' && $context['type'] != 'static') {
            // hide or show the 'read more' link
            if (empty($post['post_more']))
                $tpl->setVar('BLOCK_POST_MORE', '');
            else
                $tpl->parse('BLOCK_POST_MORE', 'post_more');
        }
        $tpl->parse('BLOCK_POST', 'post', true);
    }

    function renderComments(&$post) {
        $tpl =& $this->tpl;
        $tpl->setVar('comments_list_start', '1');
        $this->_processPlugins('render_comments', $post);
        
        if (!$tpl->setBlock('index', 'comments', 'BLOCK_COMMENTS', false))
            return;
        if ($post['skip_comments']) {
            $tpl->setVar('BLOCK_COMMENTS', '');
            return;
        }
        $comments =& $this->getComments($post['post_id']);
        if (count($comments) == 0) {
            $tpl->setBlock('comments', 'comments_list', 'BLOCK_COMMENTS_LIST');
            $tpl->setVar('BLOCK_COMMENTS_LIST', '');
            $tpl->setVar(
                "comments_message_text",
                ($post['comment_status'] == 'open')
                ? $this->messages['comments_open_nocomments']
                : $this->messages['comments_closed_nocomments']);
        } else {
            $tpl->setBlock('comments', 'comments_message', 'BLOCK_COMMENTS_MESSAGE');
            $tpl->setVar('BLOCK_COMMENTS_MESSAGE', '');
            $tpl->setBlock('comments', 'comment', 'BLOCK_COMMENT');
            $tpl->setBlock('comment', 'author_url', 'BLOCK_COMMENT_AUTHOR_URL');
            $i = 1;
            foreach ($comments as $comment) {
                $comment['comment_content'] = stripslashes($comment['comment_content']);
                $comment['comment_content'] = $this->paragrapher($comment['comment_content']);
                $comment_timestamp = $comment['comment_timestamp'];
                $comment['comment_short_date'] = strftime($this->options['short_date_format'], $comment_timestamp);
                $comment['comment_short_time'] = strftime($this->options['short_time_format'], $comment_timestamp);
                $comment['comment_date'] = strftime($this->options['date_format'], $comment_timestamp);
                $comment['comment_time'] = strftime($this->options['time_format'], $comment_timestamp);
                $comment['comment_index'] = $i;
                $comment['comment_class'] = ($i % 2 == 0) ? 'comment_even' : 'comment_odd';
                // render plugins
                $this->_processPlugins('parse_comment', $comment);
                $tpl->setVar($comment);
                if (empty($comment['comment_author_url']))
                    $tpl->setVar('BLOCK_COMMENT_AUTHOR_URL', $comment['comment_author']);
                else
                    $tpl->parse('BLOCK_COMMENT_AUTHOR_URL', 'author_url');
                $tpl->parse('BLOCK_COMMENT', 'comment', true);
                $i++;
            }
        }
        
        $tpl->parse('BLOCK_COMMENTS', 'comments');
        
    }
    
    function renderSidebar() {
        
        if (is_string($this->_sidebar))
            return $this->_sidebar;
        
        $this->startRender();
        $tpl =& $this->tpl;
        
        if ($this->context['type'] == 'search')
            $tpl->setVar('search_keywords', htmlspecialchars($this->context['filter'], ENT_QUOTES, $this->options['charset']));
        else
            $tpl->setVar('search_keywords', '');
        
        if ($this->context['type'] == 'tag') {
            $tpl->setVar('search_tags', $this->context['filter']);
            $tpl->setVar('search_tags_encoded', str_replace('%2F', '/', urlencode($this->context['filter'])));
        } else {
            $tpl->setVar('search_tags', '');
            $tpl->setVar('search_tags_encoded', '');
        }
        
        // render plugins
        $plugin_data = '';
        
        $this->_processPlugins('sidebar', $plugin_data);
        $this->_sidebar = $tpl->parse('SIDEBAR', 'sidebar');
    }
        
    function renderPage($parse_posts=true, $parse_sidebar=true, $do_output=true) {
        
        $this->startRender();
        
        $plugin_data = array(
            'parse_posts'=>$parse_posts,
            'parse_sidebar'=>$parse_sidebar,
            'do_output'=>$do_output);
        
        $this->_processPlugins('pre_render', $plugin_data);
        
        $tpl =& $this->tpl;
        
        if ($plugin_data['parse_posts']) {
            $posts =& $this->getPosts();
            
            if (count($posts) == 0) {
                // return a 404 error
                if (isset($_SERVER['SERVER_PROTOCOL']))
                    header ($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
                else
                    header ('HTTP/1.0 404 Not Found');
                // then output a page with the correct header, sidebar, etc.
				$this->tpl->setBlock('index', 'post', 'BLOCK_POST');
				if (!$tpl->setBlock('index', 'comments', 'BLOCK_COMMENTS', false))
                    $tpl->setVar('BLOCK_COMMENTS', '');
				$this->tpl->setVar('BLOCK_POST', $this->messages['no_posts']);
                $tpl->setVar('BLOCK_COMMENTS', '');
                // hide plugins
                $dummyvar = '';
                $this->_processPlugins('parse_post', $dummyvar, true);
            } else {
                foreach ($posts as $k => $v) {
                    $this->renderPost($posts[$k]);
                }
            }
        }
        if ($plugin_data['parse_sidebar']) {
            $this->renderSidebar();
        }
        
        $tpl->parse('HEADER', 'header');
        $tpl->parse('FOOTER', 'footer');
        
        if (!empty($this->context['label']))
            $this->title = "{$this->context['label']} &raquo; {$this->options['blogname']}";
        $tpl->setVar(array(
            'title'         => $this->title,
            'context_label' => $this->context['label']));
            
        $this->_processPlugins('post_render', $plugin_data);
        
        $out =& $tpl->parse('MAIN', 'index');
        
        // add a hook here if we ever need to parse the output before sending it
        // $this->_processPlugins('pre_output', $out);
        
        if (!is_null($this->cache) && $this->cache->serve_pages)
            $this->cache->setCache($out);
        
        if ($plugin_data['do_output'])
            echo $out;
        
        $this->_processPlugins('shutdown', $plugin_data);
        
    }
    
    /***************************************************************************
     * utilities
     **************************************************************************/

    function &getMailHelper() {
        if (!is_object($this->_mail_helper)) {
            require_once 'classes/MailHelper.php';
            $o =& $this->options;
            $this->_mail_helper =& new MailHelper(
                $o['mail_from'],
                $o['mail_mailer'],
                (isset($o['mail_mailer_path']) ? $o['mail_mailer_path'] : null));
        }
        return $this->_mail_helper;
    }
    
    /***************************************************************************
     * comments stuff
     **************************************************************************/
    
    function insertComment(&$comment, $xml_output=false) {
    
        $options =& $this->options;
        $tables =& $this->tables;
        $db =& $this->db;
        
        $this->_processPlugins('insert_comment', $comment);
        
        // fix url
        if (!empty($comment['url']) && !preg_match('/^[a-zA-Z]+:\/\//', $comment['url']))
            $comment['url'] = 'http://' . $comment['url'];
        
        $comment['comment'] = strip_tags($comment['comment'], $options['allowed_tags']);
        $comment['comment'] = $this->stripAttrs($comment['comment']);
        $c_text = addslashes($comment['comment']);
        
        $gmdate = gmdate("Y-m-d H:i:s", $comment['date']);
        
        $q = "insert into {$tables['comments']} (
                comment_post_ID, comment_author, comment_author_email, comment_author_url,
                comment_author_IP, comment_date, comment_date_gmt, comment_content,
                comment_approved, comment_agent, comment_type) 
            VALUES ({$comment['post_id']}, '" . addslashes($comment['author']) . "', '" . addslashes($comment['email']) . "', " .
                "'" . addslashes($comment['url']) . "', '{$_SERVER['REMOTE_ADDR']}', " .
                "'" . strftime("%Y-%m-%d %H:%M:%S", $comment['date']) . "', '$gmdate', '$c_text', " .
                "'{$comment['approved']}', '{$_SERVER['HTTP_USER_AGENT']}', '{$comment['type']}')";
        
        $db->query($q, true);
        
        if ($comment['approved'] == 1)
            $db->query("update {$tables['posts']} set comment_count = comment_count + 1 where id = {$comment['post_id']}");
        
        // trigger the cache garbage collector if we have an active cache
        if (!is_null($this->cache) && $this->cache->active) {
            if (isset($options['cache_actions_all_pages']) && $options['cache_actions_all_pages'] == '1')
                $this->cache->garbageCollect(null, true);
            else
                $this->cache->clearCachePostUri($_SERVER['REQUEST_URI'], true);
        }
        
        // TODO: move to the DB wrapper
        $comment['id'] = mysql_insert_id();
        
    }
    
    function stripAttrs($string, $allowed=array('title', 'href', '/')) {
        $m = array();
        if (preg_match_all('/<([a-zA-Z]+)\s+([^>]+)>/sS', $string, $m, PREG_SET_ORDER)) {
            foreach ($m as $match) {
                $tag = '<' . $match[1];
                foreach (preg_split('/\s+/', $match[2], -1, PREG_SPLIT_NO_EMPTY) as $attr) {
                    $attr_tokens = explode('=', $attr);
                    if (in_array($attr_tokens[0], $allowed)) {
                        $tag .= ' ' . $attr;
                    }
                }
                $tag .= '>';
                $string = str_replace($match[0], $tag, $string);
            }
        }
        return $string;
    }

    function linebreaker(&$s) {
        if (!preg_match('@<[^>]+\n@sm', $s))
            return str_replace("\n", "<br />\n", $s);
        $buffer = '';
        $inside = false;
        for ($i=0;$i<strlen($s);$i++) {
            $c = $s[$i];
            switch ($c) {
                case '<':
                    $inside = true;
                    $buffer .= $c;
                    break;
                case '>':
                    $inside = false;
                    $buffer .= $c;
                    break;
                case "\n":
                    $buffer .= ($inside ? "\n" : "<br />\n");
                    break;
                default:
                    $buffer .= $c;
            }
        }
        return $buffer;
    }

    function &paragrapher(&$s) {
        
        if (substr($s, 0, 4) == "\t<p>")
            return($s);    // already formatted
        
        // clean up bare &
        $s = preg_replace('@&(?![a-z0-9#]+;)@', '&amp;', $s);
        
        // code should be inside pre to be preformatted, let's leave it here for now anyway
        $block_tags = 'object|pre|p|dl|div|noscript|script|blockquote|form|table|td|th|ins|fieldset|address|h1|h2|h3|h4|h5|h6|ul|ol|li|code';
        
        $r = '@(?:
            # match paragraph mark
            ((?:\n\s*){2,})
        | # match block open tag
        (<                          # save tag
        (' . $block_tags . ')   # save tag name
        [^>]*>)
        | # match block close tag
        (</\s*(' . $block_tags . ')\s*>)
        )@smix';

        $m = array();
    
        preg_match_all($r, str_replace("\r", '', $s), $m, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);

        $matches = count($m);
    
        $s = str_replace("\r", '', $s);
    
        if ($matches == 0) {
            $buffer = "<p>" . Frontend::linebreaker($s) . "</p>";
            return $buffer;
        } else {
            $p_parents = array('div'=>1, 'blockquote'=>1, 'td'=>1, 'th'=>1, 'ins'=>1, 'form'=>1, 'li'=>1);
            $buffer = '';
            $pre = false;
            $last_offset = 0;
            $tags = array();
            $tag = null;
            $tag_content = '';
            $reset_tag = null;
        
            foreach ($m as $match) {

                // grab the content from the latest match offset up to the current one
                $offset = $match[0][1];
                $slice = substr($s, $last_offset, $offset - $last_offset);
                $convert = is_null($tag) || (!$pre && isset($p_parents[$tag]));
                if ($convert) {
                    $slice = trim($slice);
                    $slice = Frontend::linebreaker($slice);
                }
                $last_offset = $offset + strlen($match[0][0]);

                // now fill the buffer
                if (!empty($slice))
                    $buffer .= $convert ? "<p>$slice</p>\n" : "$slice";
            
                // set the current tag context
                switch (count($match)) {
                    case 2:
                        # paragraph mark
                        $buffer .= $pre ? "\n\n" : '';
                        break;
                    case 4:
                        # block open tag
                        $tag = strtolower($match[3][0]);
                        $tags[] = $tag;
                        if ($tag == 'pre' || $tag == 'script')
                            $pre = true;
                        $buffer .= $match[0][0] . ($pre ? '' : "\n");
                        break;
                    case 6:
                        # block close tag
                        array_pop($tags);
                        $tag = strtolower($match[5][0]);
                        if ($tag == 'pre' || $tag == 'script')
                            $pre = false;
                        $buffer .= $match[0][0] . ($pre ? '' : "\n");
                        if ($tag == 'pre')
                            $pre = false;
                        $tlen = count($tags);
                        if ($tlen > 0)
                            $tag = $tags[$tlen - 1];
                        else
                            $tag = $pre = null;
                        break;
                }

            }
        
            $tail = substr($s, $last_offset, strlen($s) - $last_offset);
            if ($pre) {
                // unlikely, but does not hurt to check
                $buffer .= $tail;
            } else {
                $tail = trim($tail);
                if (!empty($tail))
                    $buffer .= '<p>' . Frontend::linebreaker($tail) . '</p>';
            }
        }
        
        return $buffer;
    }

    function setUserCookie($user_data) {
        $cookiepath = preg_replace('|https?://[^/]+|i', '', $this->options['url'] . '/');
        foreach ($user_data as $k=>$v) {
            switch ($k) {
                case 'ud_name':
                case 'ud_email':
                case 'ud_url':
                    break;
                default:
                    unset($user_data[$k]);
            }
        }
        setcookie('lp_identity', serialize($user_data), time() + 30000000, $cookiepath);
    }
    
    function removeUserCookie() {
        $cookiepath = preg_replace('|https?://[^/]+|i', '', $this->options['url'] . '/');
        setcookie('lp_identity', '', time() + 30000000, $cookiepath);
    }
    
    function &getUserPerms($filter='', $extra_fields=null) {
        
        $q = "select u.id, u.user_email, u.user_login, um.meta_key, um.meta_value";
        if (is_array($extra_fields))
            $q .= ', ' . implode(', ', $extra_fields);
        $q .= " from {$this->tables_prefix}_users u
                inner join {$this->tables_prefix}_usermeta um on u.ID = um.user_id
                where ";
        if (!empty($filter))
            $q .= "$filter and ";
        $q .= "um.meta_key in ('{$this->tables_prefix}_user_level', '{$this->tables_prefix}_capabilities')";
        
        $users = array();
        
        if (!$this->db->query($q))
            return $users;
        
        foreach ($this->db->all() as $row) {
            $id = $row['id'];
            if (!isset($users['id'])) {
                $users[$id] = array(
                    'user_email'=>$row['user_email'],
                    'user_login'=>$row['user_login'],
                    'user_level'=>null,
                    'capabilities'=>array());
                if (is_array($extra_fields)) {
                    foreach ($extra_fields as $field)
                        $users[$id][$field] = $row[$field];
                }
            }
            $meta_key = substr($row['meta_key'], strlen($this->tables_prefix) + 1);
            $users[$id][$meta_key] = $row['meta_value'];
        }
        foreach ($users as $k=>$v) {
            if (!is_int($users[$k]['user_level']))
                $users[$k]['user_level'] = (int)$users[$k]['user_level'];
            if (!is_array($users[$k]['capabilities']))
                $users[$k]['capabilities'] = unserialize($users[$k]['capabilities']);
        }
        
        return $users;
        
    }
    
    function userFromWpCookie() {
        
        if (!is_null($this->_user_from_cookie))
            return $this->_user_from_cookie;
        
        $cookie_hash = md5($this->options['wp_url']);
        $user_cookie = "wordpressuser_$cookie_hash";
        $pass_cookie = "wordpresspass_$cookie_hash";
        
        if (!isset($_COOKIE[$user_cookie]) || !isset($_COOKIE[$pass_cookie]))
            return;
        
        $user_login = $_COOKIE[$user_cookie];
        $user_md5_pass = $_COOKIE[$pass_cookie];
        
        $users =& $this->getUserPerms("u.user_login = '$user_login' and md5(u.user_pass) = '$user_md5_pass'");

        if (count($users) == 0)
            return;
        $this->_user_from_cookie = array_shift($users);
        return $this->_user_from_cookie;
        
    }
    
    function redirectToPost($post_permalink, $message) {
        // redirect user so that he does not insert the comment twice
        header(
            "Location: " .
            $this->options['url'] . '/' .
            $post_permalink . '?' .
            'message=' . urlencode($message) .
            '#add_comment');
        exit;
    }
    
    function &_extractKeywords($q, $force=false) {
        if (!$force && !is_null($this->search_keywords))
            return $this->search_keywords;
        // sanitize
        $q = trim($q);
        $q = preg_replace('/\s+/', ' ', $q);
        // extract keywords
        $keywords = array();
        $inside = false;
        $keyword = '';
        for ($i = 0; $i < strlen($q); $i++) {
            $c = $q[$i];
            if ($inside) {
                if ($c == '"')
                    $inside = false;
                else
                    $keyword .= $c;
            } else {
                switch ($c) {
                    case ' ':
                        if (!empty($keyword))
                            $keywords[] = addslashes(strtolower($keyword));
                        $keyword = '';
                        break;
                    case '"':
                        $inside = true;
                        break;
                    default:
                        $keyword .= $c;
                }
            }
        }
        if (!empty($keyword))
            $keywords[] = addslashes(strtolower($keyword));
        $this->search_keywords =& $keywords;
        return $keywords;
    }
    
    /***************************************************************************
     * feeds stuff
     **************************************************************************/
    
    function doConditionalGet($tstamp) {
        /* A PHP implementation of conditional get for LightPress, see also
         * http://fishbowl.pastiche.org/archives/001132.html
         * http://simon.incutio.com/archive/2003/04/23/conditionalGet
         *
         * for HTTP/1.1 dates see RFC2616#3.3.1
         *
         * Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
         * Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
         * Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
         * All HTTP date/time stamps MUST be represented in Greenwich Mean Time
         * (GMT), without exception.
         * HTTP/1.1 clients and servers that parse the date value MUST accept
         * all three formats (for compatibility with HTTP/1.0), though they MUST
         * only generate the RFC 1123 format for representing HTTP-date values
         * in header fields.
         *
         * and #14.25 for If-Modified-Since
         * 
         * Note: When handling an If-Modified-Since header field, some
         * servers will use an exact date comparison function, rather than a
         * less-than function, for deciding whether to send a 304 (Not
         * Modified) response. To get best results when sending an If-
         * Modified-Since header field for cache validation, clients are
         * advised to use the exact date string received in a previous Last-
         * Modified header field whenever possible.
         *
         * etc.
         *
         */
        
        // ETag is any quoted string
        $etag = '"'. $tstamp .'"';
        
        // RFC1123 date, see http://bugs.php.net/bug.php?id=31842
        if (version_compare(PHP_VERSION, "4.3.11", ">="))
            $format = 'r';
        else
            $format = 'D, d M Y H:i:s O';
        $rfc1123 = substr(date($format, $tstamp), 0, -5) . 'GMT';
        
        // RFC1036 date
        $rfc1036 = date('l, d-M-y H:i:s ', $tstamp) . 'GMT';
        
        // asctime
        $ctime = date('D M j H:i:s', $tstamp);
        
        // Send the headers
        header("Last-Modified: $rfc1123");
        header("ETag: $etag");
        
        // See if the client has provided the required headers
        
        $if_modified_since = $if_none_match = false;
        
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
            $if_modified_since = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
        
        if(isset($_SERVER['HTTP_IF_NONE_MATCH']))
            $if_none_match = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);

        if (!$if_modified_since && !$if_none_match) {
            // both are missing
            return $rfc1123;
        }
        
        // At least one of the headers is there - check them
        
        // check etag if it's there and there's no if-modified-since
        if ($if_none_match) {
            if ($if_none_match != $etag) {
                // etag is there but doesn't match
                return $rfc1123;
            }
            if (!$if_modified_since && ($if_none_match == $etag)) {
                    header('HTTP/1.0 304 Not Modified');
                    exit;
            }
        }
        
        if ($if_modified_since) {
            // check if-modified-since
        
            foreach (array($rfc1123, $rfc1036, $ctime) as $d) {
                if ($d == $if_modified_since) {
                    // Nothing has changed since their last request - serve a 304 and exit
                    header('HTTP/1.0 304 Not Modified');
                    exit;
                }
            }
        }
        
        // return $rfc1123 as it may be useful later, eg 'lastBuildDate' for RSS2
        return $rfc1123;
        
    }
    
    function ISO8601($tstamp) {
        return date("Y-m-d\TH:i:s", $tstamp) . '+00:00';
    }    
    
    /***************************************************************************
     * moved from attributes to (static) methods for the WP admin plugins
     **************************************************************************/
    
    function getCriticalOptions() {
        return array('basedir', 'url', 'mail_from');
    }
    
    function getBaseOptions() {
        return array(
            //'basedir'         => dirname(__FILE__),       // can't live without
            //'url'             => 'http://myhost.org',     // can't live without
            'wp_url'            => 'http://myhost.org/wordpress',   // set by Frontend if not in config.php
            'blogname'          => 'Blog Name',
            'shortname'         => 'short_name',
            'description'       => 'Description for atom feeds.',
            'copyright_notice'  => 'Copyright notice for atom feeds.',
            'posts_per_page'    => 12,                           
            'posts_per_rss'     => 10,
            'rss_use_excerpt'   => '1',
            'date_format'       => '%A %d %B %Y',
            'short_date_format' => '%d %b %Y',
            'time_format'       => '%H:%M:%S',
            'short_time_format' => '%H:%M',
            'archive_label'     => '%B %Y',
            'lang'              => 'en_US',
            'charset'           => 'UTF-8',            // UTF-8, ISO-8859-1, etc.    
            'template'          => 'k1andahalf',
            'approx_sql_tstamp' => 100, // where_date = round(where_date / approx_sql_tstamp) * approx_sql_tstamp
            'mail_mailer'       => false,    // false or empty to use PHP's mail() function
            //'mail_from'       => 'me@my.host',            // can't live without
            'mail_on_comment'   => true,
            'allowed_tags'      => '<a> <acronym> <p> <br> <b> <blockquote> <code> <em> <i> <strike> <strong>',
            'post_prefix'       => 'post/{post_name}',
            'category_prefix'   => 'category',
            'archives_prefix'   => 'archives/{year}/{monthnum}',
            'pages_prefix'      => '',
            'pages_suffix'      => '',
            'search_prefix'     => 'search',
            'author_prefix'     => 'author',
            'tag_prefix'        => 'tag',
            'comments_prefix'   => 'comments',
            'category_separator'=> ',',
            'comment_spam_kill' => true,                // true kills the supposed spam, false inserts it waiting for moderation
            'comment_spam_mail' => true,                // true mail on kills only
            'use_preformatter'  => false,
            'preformatter_method' => 'wp',
            'plugins'           => array()
        );
    }
    
    function getDefaultPlugins() {
        // try to list them all here so we can include the template tags for inactive plugins
        return array(
            // plugin                   active contexts                             args
            'AboutContext'              =>array(LP_CONTEXT_CATEGORY | LP_CONTEXT_ARCHIVES,  array()),
            'AboutMe'                   =>array(LP_CONTEXT_INDEX,                           array()),
            'AboutPost'                 =>array(LP_CONTEXT_POST,                            array()),
            'AdminOptions'              =>array(LP_CONTEXT_ALL,                             array()),
            'AltTemplate'               =>array(0,                                          array()),
            'ArchivesList'              =>array(0,                                          array()),
            'ArchivesListExtended'      =>array(LP_CONTEXT_ALL,                             array()),
            'AuthorIndex'               =>array(LP_CONTEXT_AUTHOR,                          array()),
            'CategoriesList'            =>array(0,                                          array()),
            'CategoriesListExtended'    =>array(LP_CONTEXT_ALL,                             array()),
            'CommentsList'              =>array(0,                                          array()),
            'CommentSpam'               =>array(LP_CONTEXT_POST,                            array()),
            'Delicious'                 =>array(0,                                          array()),
            'Flickr'                    =>array(0,                                          array()),
            'Gallery'                   =>array(0,                                          array()),
            'HighlightPost'             =>array(0,                                          array()),
            'HighlightImage'            =>array(0,                                          array()),
            'HeaderAd'                  =>array(0,                                          array()),
            'Links'                     =>array(LP_CONTEXT_ALL,                             array()),
            'MetaDescription'           =>array(0,                                          array()),
            'NestedPages'               =>array(0,                                          array()),
            'Pagination'                =>array(LP_CONTEXT_ALL,                             array()),
            'PostComment'               =>array(LP_CONTEXT_POST,                            array()),
            'PostNavigation'            =>array(0,                                          array()),
            'PostRelated'               =>array(LP_CONTEXT_POST,                            array()),
            'PostsList'                 =>array(LP_CONTEXT_ALL ^ LP_CONTEXT_INDEX,          array()),
            'PostTags'                  =>array(LP_CONTEXT_ALL,                             array()),
            'RandomPosts'               =>array(0,                                          array()),
            'SearchIndex'               =>array(LP_CONTEXT_SEARCH,                          array()),
            'SecondPage'                =>array(0,                                          array()),
            'Stats'                     =>array(0,                                          array()),
            'TagCloud'                  =>array(LP_CONTEXT_STATIC,                          array()),
            'TagMetaKeywords'           =>array(0,                                          array()),
            'TopCommented'              =>array(LP_CONTEXT_ALL,                             array()),
            'TopRead'                   =>array(0,                                          array()),
            );
    }

    function getVersion() {
        return '2.0.0';
    }

    function getPermalinkVerbs() {
        return array(
            'post'      => array(
                'year'          => '%Y', 
                'monthnum'      => '%m', 
                'day'           => '%d',
                'hour'          => '%H',
                'minute'        => '%M',
                'second'        => '%S',
                'postname'      => '%%1$s',
                'post_name'     => '%%1$s',
                'post_id'       => '%%2$s',
                'category_id'   => '%%3$s',
                'category_name' => '%%4$s'),
            // 'author'        => ''), // unsupported for now, since it requires an additional JOIN in plugins
            'archives'  => array(
                'year'          => '%Y', 
                'monthnum'      => '%m', 
                'day'           => '%d'));
    }
    
    function prefixToPermalinkStruct($value, $context='post') {
        
        if (empty($value))
            return $value;
        
        $permalink_verbs = Frontend::getPermalinkVerbs();

        if (!isset($permalink_verbs[$context]))
                return $value;

        $matches = array();
        preg_match_all('/{([^}]+)}/', $value, $matches);
        foreach ($matches[1] as $i=>$verb) {
            if (isset($permalink_verbs[$context][$verb])) {
                $value = str_replace('{' . $verb . '}', $permalink_verbs[$context][$verb], $value);
            }
        }
        
        return $value;

    }
    
    function seems_utf8($Str) {
        // by bmorel at ssi dot fr
        for ($i=0; $i<strlen($Str); $i++) {
            if (ord($Str[$i]) < 0x80) continue; # 0bbbbbbb
            elseif ((ord($Str[$i]) & 0xE0) == 0xC0) $n=1; # 110bbbbb
            elseif ((ord($Str[$i]) & 0xF0) == 0xE0) $n=2; # 1110bbbb
            elseif ((ord($Str[$i]) & 0xF8) == 0xF0) $n=3; # 11110bbb
            elseif ((ord($Str[$i]) & 0xFC) == 0xF8) $n=4; # 111110bb
            elseif ((ord($Str[$i]) & 0xFE) == 0xFC) $n=5; # 1111110b
            else return false; # Does not match any model
            for ($j=0; $j<$n; $j++) { # n bytes matching 10bbbbbb follow ?
                if ((++$i == strlen($Str)) || ((ord($Str[$i]) & 0xC0) != 0x80))
                    return false;
            }
        }
        return true;
    }
    
    function remove_accents($string) {
        if ($this->seems_utf8($string)) {
            $chars = array(
                // Decompositions for Latin-1 Supplement
                chr(195).chr(128) => 'A', chr(195).chr(129) => 'A',
                chr(195).chr(130) => 'A', chr(195).chr(131) => 'A',
                chr(195).chr(132) => 'A', chr(195).chr(133) => 'A',
                chr(195).chr(135) => 'C', chr(195).chr(136) => 'E',
                chr(195).chr(137) => 'E', chr(195).chr(138) => 'E',
                chr(195).chr(139) => 'E', chr(195).chr(140) => 'I',
                chr(195).chr(141) => 'I', chr(195).chr(142) => 'I',
                chr(195).chr(143) => 'I', chr(195).chr(145) => 'N',
                chr(195).chr(146) => 'O', chr(195).chr(147) => 'O',
                chr(195).chr(148) => 'O', chr(195).chr(149) => 'O',
                chr(195).chr(150) => 'O', chr(195).chr(153) => 'U',
                chr(195).chr(154) => 'U', chr(195).chr(155) => 'U',
                chr(195).chr(156) => 'U', chr(195).chr(157) => 'Y',
                chr(195).chr(159) => 's', chr(195).chr(160) => 'a',
                chr(195).chr(161) => 'a', chr(195).chr(162) => 'a',
                chr(195).chr(163) => 'a', chr(195).chr(164) => 'a',
                chr(195).chr(165) => 'a', chr(195).chr(167) => 'c',
                chr(195).chr(168) => 'e', chr(195).chr(169) => 'e',
                chr(195).chr(170) => 'e', chr(195).chr(171) => 'e',
                chr(195).chr(172) => 'i', chr(195).chr(173) => 'i',
                chr(195).chr(174) => 'i', chr(195).chr(175) => 'i',
                chr(195).chr(177) => 'n', chr(195).chr(178) => 'o',
                chr(195).chr(179) => 'o', chr(195).chr(180) => 'o',
                chr(195).chr(181) => 'o', chr(195).chr(182) => 'o',
                chr(195).chr(182) => 'o', chr(195).chr(185) => 'u',
                chr(195).chr(186) => 'u', chr(195).chr(187) => 'u',
                chr(195).chr(188) => 'u', chr(195).chr(189) => 'y',
                chr(195).chr(191) => 'y',
                // Decompositions for Latin Extended-A
                chr(196).chr(128) => 'A', chr(196).chr(129) => 'a',
                chr(196).chr(130) => 'A', chr(196).chr(131) => 'a',
                chr(196).chr(132) => 'A', chr(196).chr(133) => 'a',
                chr(196).chr(134) => 'C', chr(196).chr(135) => 'c',
                chr(196).chr(136) => 'C', chr(196).chr(137) => 'c',
                chr(196).chr(138) => 'C', chr(196).chr(139) => 'c',
                chr(196).chr(140) => 'C', chr(196).chr(141) => 'c',
                chr(196).chr(142) => 'D', chr(196).chr(143) => 'd',
                chr(196).chr(144) => 'D', chr(196).chr(145) => 'd',
                chr(196).chr(146) => 'E', chr(196).chr(147) => 'e',
                chr(196).chr(148) => 'E', chr(196).chr(149) => 'e',
                chr(196).chr(150) => 'E', chr(196).chr(151) => 'e',
                chr(196).chr(152) => 'E', chr(196).chr(153) => 'e',
                chr(196).chr(154) => 'E', chr(196).chr(155) => 'e',
                chr(196).chr(156) => 'G', chr(196).chr(157) => 'g',
                chr(196).chr(158) => 'G', chr(196).chr(159) => 'g',
                chr(196).chr(160) => 'G', chr(196).chr(161) => 'g',
                chr(196).chr(162) => 'G', chr(196).chr(163) => 'g',
                chr(196).chr(164) => 'H', chr(196).chr(165) => 'h',
                chr(196).chr(166) => 'H', chr(196).chr(167) => 'h',
                chr(196).chr(168) => 'I', chr(196).chr(169) => 'i',
                chr(196).chr(170) => 'I', chr(196).chr(171) => 'i',
                chr(196).chr(172) => 'I', chr(196).chr(173) => 'i',
                chr(196).chr(174) => 'I', chr(196).chr(175) => 'i',
                chr(196).chr(176) => 'I', chr(196).chr(177) => 'i',
                chr(196).chr(178) => 'IJ',chr(196).chr(179) => 'ij',
                chr(196).chr(180) => 'J', chr(196).chr(181) => 'j',
                chr(196).chr(182) => 'K', chr(196).chr(183) => 'k',
                chr(196).chr(184) => 'k', chr(196).chr(185) => 'L',
                chr(196).chr(186) => 'l', chr(196).chr(187) => 'L',
                chr(196).chr(188) => 'l', chr(196).chr(189) => 'L',
                chr(196).chr(190) => 'l', chr(196).chr(191) => 'L',
                chr(197).chr(128) => 'l', chr(197).chr(129) => 'L',
                chr(197).chr(130) => 'l', chr(197).chr(131) => 'N',
                chr(197).chr(132) => 'n', chr(197).chr(133) => 'N',
                chr(197).chr(134) => 'n', chr(197).chr(135) => 'N',
                chr(197).chr(136) => 'n', chr(197).chr(137) => 'N',
                chr(197).chr(138) => 'n', chr(197).chr(139) => 'N',
                chr(197).chr(140) => 'O', chr(197).chr(141) => 'o',
                chr(197).chr(142) => 'O', chr(197).chr(143) => 'o',
                chr(197).chr(144) => 'O', chr(197).chr(145) => 'o',
                chr(197).chr(146) => 'OE',chr(197).chr(147) => 'oe',
                chr(197).chr(148) => 'R',chr(197).chr(149) => 'r',
                chr(197).chr(150) => 'R',chr(197).chr(151) => 'r',
                chr(197).chr(152) => 'R',chr(197).chr(153) => 'r',
                chr(197).chr(154) => 'S',chr(197).chr(155) => 's',
                chr(197).chr(156) => 'S',chr(197).chr(157) => 's',
                chr(197).chr(158) => 'S',chr(197).chr(159) => 's',
                chr(197).chr(160) => 'S', chr(197).chr(161) => 's',
                chr(197).chr(162) => 'T', chr(197).chr(163) => 't',
                chr(197).chr(164) => 'T', chr(197).chr(165) => 't',
                chr(197).chr(166) => 'T', chr(197).chr(167) => 't',
                chr(197).chr(168) => 'U', chr(197).chr(169) => 'u',
                chr(197).chr(170) => 'U', chr(197).chr(171) => 'u',
                chr(197).chr(172) => 'U', chr(197).chr(173) => 'u',
                chr(197).chr(174) => 'U', chr(197).chr(175) => 'u',
                chr(197).chr(176) => 'U', chr(197).chr(177) => 'u',
                chr(197).chr(178) => 'U', chr(197).chr(179) => 'u',
                chr(197).chr(180) => 'W', chr(197).chr(181) => 'w',
                chr(197).chr(182) => 'Y', chr(197).chr(183) => 'y',
                chr(197).chr(184) => 'Y', chr(197).chr(185) => 'Z',
                chr(197).chr(186) => 'z', chr(197).chr(187) => 'Z',
                chr(197).chr(188) => 'z', chr(197).chr(189) => 'Z',
                chr(197).chr(190) => 'z', chr(197).chr(191) => 's',
                // Euro Sign
                chr(226).chr(130).chr(172) => 'E');
 
            $string = strtr($string, $chars);
        } else {
            // Assume ISO-8859-1 if not UTF-8
            $chars['in'] = chr(128).chr(131).chr(138).chr(142).chr(154).chr(158)
            .chr(159).chr(162).chr(165).chr(181).chr(192).chr(193).chr(194)
            .chr(195).chr(196).chr(197).chr(199).chr(200).chr(201).chr(202)
            .chr(203).chr(204).chr(205).chr(206).chr(207).chr(209).chr(210)
            .chr(211).chr(212).chr(213).chr(214).chr(216).chr(217).chr(218)
            .chr(219).chr(220).chr(221).chr(224).chr(225).chr(226).chr(227)
            .chr(228).chr(229).chr(231).chr(232).chr(233).chr(234).chr(235)
            .chr(236).chr(237).chr(238).chr(239).chr(241).chr(242).chr(243)
            .chr(244).chr(245).chr(246).chr(248).chr(249).chr(250).chr(251)
            .chr(252).chr(253).chr(255);
 
            $chars['out'] = "EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy";
 
            $string = strtr($string, $chars['in'], $chars['out']);
            $double_chars['in'] = array(chr(140), chr(156), chr(198), chr(208), chr(222), chr(223), chr(230), chr(240), chr(254));
            $double_chars['out'] = array('OE', 'oe', 'AE', 'DH', 'TH', 'ss', 'ae', 'dh', 'th');
            $string = str_replace($double_chars['in'], $double_chars['out'], $string);
        }
 
        return $string;
    }
 
    function sanitize_user($user) {
 
        $user = $this->remove_accents($user);
        $user = strtolower($user);
        $user = preg_replace('/\s+/', '-', $user);
 
        return $user;
    }

    function raiseError($message, $method, $line, $type=E_USER_WARNING) {
        trigger_error(
            sprintf("%s.%s() line %d: %s", get_class($this), $method, $line, $message),
            $type);
    }

}

?>
